package com.yangrd.airship.less;

import com.alibaba.fastjson.JSON;
import com.github.wenhao.jpa.PredicateBuilder;
import com.github.wenhao.jpa.Specifications;
import com.yangrd.airship.common.spring.SpringContextHolder;
import com.yangrd.airship.generator.page.PageGeneratorDefine;
import com.yangrd.airship.generator.page.impl.factory.clazz.ClassPageGeneratorDefineFactory;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.logging.log4j.util.Strings;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author yangrd
 * @date 2021/12/09
 */
@Configuration
public class LessApi {

    @Bean
    public LessController lessController(LessManager lessManager) {
        return new LessController(lessManager);
    }

    @Bean
    public LessManager lessManager() {
        return new LessManager(
                new RepositoryRegistry(),
                new EntityRegistry(),
                new EntityIdRegistry()
        );
    }


    @RestController
    @RequestMapping("/api/less-page-define")
    @RequiredArgsConstructor
    public static class LessPageDefineController {
        private final LessManager lessManager;
        private final ClassPageGeneratorDefineFactory classPageGeneratorDefineFactory;
        private Map<String, PageGeneratorDefine> entity2PageDefineCache = new HashMap<>();

        @GetMapping("/{entity}")
        public PageGeneratorDefine getConfig(@PathVariable String entity) {
            return entity2PageDefineCache.computeIfAbsent(entity, k -> classPageGeneratorDefineFactory.create(lessManager.getEntityClass(entity)));
        }

        @GetMapping("/entity-name")
        public Collection<String> listEntityName() {
            return lessManager.listEntityName();
        }
    }

    @RestController("lessController")
    @RequestMapping("/api")

    @RequiredArgsConstructor
    public static class LessController {

        private final LessManager lessManager;

        @PostMapping("/{entity}")
        @ResponseStatus(HttpStatus.CREATED)
        public void save(@PathVariable String entity, @RequestBody String t) {
            lessManager.findRepo(entity).orElseThrow(RuntimeException::new).saveAndFlush(lessManager.getEntityObj(entity, t));
        }

        @DeleteMapping("/{entity}/{id}")
        @ResponseStatus(HttpStatus.NO_CONTENT)
        public void delete(@PathVariable String entity, @PathVariable String id) {
            lessManager.findRepo(entity).orElseThrow(RuntimeException::new).deleteById(lessManager.getEntityIdObj(entity, id));
        }

        @PutMapping("/{entity}/{id}")
        @ResponseStatus(HttpStatus.NO_CONTENT)
        public void update(@PathVariable String entity, @PathVariable String id, @RequestBody String t) {
            JpaRepository<Object, Object> repository = lessManager.findRepo(entity).orElseThrow(RuntimeException::new);
            repository.findById(lessManager.getEntityIdObj(entity, id)).ifPresent(v -> {
                BeanUtils.copyProperties(lessManager.getEntityObj(entity, t), v);
                repository.saveAndFlush(v);
            });
        }

        @GetMapping("/{entity}/{id}")
        public Object get(@PathVariable String entity, @PathVariable String id) {
            return lessManager.findRepo(entity).orElseThrow(RuntimeException::new).findById(lessManager.getEntityIdObj(entity, id));
        }

        @GetMapping("/{entity}")
        public Page findAll(@PathVariable String entity, Pageable pageable, HttpServletRequest request) {
            JpaRepository<Object, Object> objectObjectJpaRepository = lessManager.findRepo(entity).orElseThrow(RuntimeException::new);
            if (objectObjectJpaRepository instanceof JpaSpecificationExecutor) {
                return ((JpaSpecificationExecutor) objectObjectJpaRepository).findAll(DomainSpecSupport.toSpec(lessManager.getEntityClass(entity), request), pageable);
            }
            return objectObjectJpaRepository.findAll(pageable);
        }
    }

    /**
     * description: DomainSpecSupport
     *
     * @author: yangrd
     * @date: 2020/9/14
     * @version: 1.0
     */
    public static class DomainSpecSupport {

        private static final String DATE_PATTERN = "yyyy-MM-dd";

        public static Specification<?> toSpec(Class<?> tClass, HttpServletRequest request) {
            Map<String, String[]> parameterMap = request.getParameterMap();
            PredicateBuilder<?> builder = Specifications.and();
            Map<String, Field> fieldMap = ReflectionUtils.listAllField(tClass).stream().collect(Collectors.toMap(Field::getName, Function.identity()));
            parameterMap.forEach((name, values) -> {
                name = name.replaceAll("\\$", ".");
                String value = values[0];
                if (isFieldPresent(name, fieldMap) && Strings.isNotBlank(value)) {
                    Field field = getField(name, fieldMap);
                    Class<?> fieldType = field.getType();
                    if (fieldType.equals(String.class)) {
                        builder.like(name, "%" + value + "%");
                    } else if (fieldType.equals(Boolean.class)) {
                        builder.eq(name, value.equals("true"));
                    } else if (fieldType.equals(Date.class)) {
                        builder.between(name, getDate(value.split(",")[0]), getDate(value.split(",")[1]));
                    } else if (fieldType.equals(LocalDateTime.class)) {
                        builder.between(name, getDate(value.split(",")[0]).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(), getDate(value.split(",")[1]).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
                    } else if (fieldType.equals(LocalDate.class)) {
                        builder.between(name, getDate(value.split(",")[0]).toInstant().atZone(ZoneId.systemDefault()).toLocalDate(), getDate(value.split(",")[1]).toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
                    } else if (Number.class.isAssignableFrom(fieldType)) {
                        builder.eq(name, Integer.valueOf(value));
                    } else if (Enum.class.isAssignableFrom(fieldType)) {
                        builder.eq(name, Enum.valueOf((Class<Enum>) fieldType, values[0]));
                    }
                }
            });
            return builder.build();
        }

        private static Date getDate(String value1) {
            try {
                return DateUtils.parseDate(value1, DATE_PATTERN);
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return null;
        }

        private static boolean isFieldPresent(String fieldName, Map<String, Field> fieldMap) {
            if (fieldName.contains(".")) {
                int i = fieldName.indexOf(".");
                String leftName = fieldName.substring(0, i);
                String rightName = fieldName.substring(i + 1);
                if (fieldMap.containsKey(leftName)) {
                    Class<?> aClass = fieldMap.get(leftName).getType();
                    if (Collection.class.isAssignableFrom(aClass)) {
                        Field field = fieldMap.get(leftName);
                        Type genericType = field.getGenericType();
                        if (genericType instanceof ParameterizedType) {
                            Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
                            aClass = (Class<?>) actualTypeArguments[0];
                        }
                    }
                    Map<String, Field> childFieldMap = ReflectionUtils.listAllField(aClass).stream().collect(Collectors.toMap(Field::getName, Function.identity()));
                    return isFieldPresent(rightName, childFieldMap);
                }
            }
            return fieldMap.containsKey(fieldName);
        }


        private static Field getField(String fieldName, Map<String, Field> fieldMap) {
            if (fieldName.contains(".")) {
                int i = fieldName.indexOf(".");
                String leftName = fieldName.substring(0, i);
                String rightName = fieldName.substring(i + 1);
                if (fieldMap.containsKey(leftName)) {
                    Class<?> aClass = fieldMap.get(leftName).getType();
                    if (Collection.class.isAssignableFrom(aClass)) {
                        Field field = fieldMap.get(leftName);
                        Type genericType = field.getGenericType();
                        if (genericType instanceof ParameterizedType) {
                            Type[] actualTypeArguments = ((ParameterizedType) genericType).getActualTypeArguments();
                            aClass = (Class<?>) actualTypeArguments[0];
                        }
                    }
                    Map<String, Field> childFieldMap = ReflectionUtils.listAllField(aClass).stream().collect(Collectors.toMap(Field::getName, Function.identity()));
                    return getField(rightName, childFieldMap);
                }
            }
            return fieldMap.get(fieldName);
        }

    }

    public static class ReflectionUtils {

        public static List<Field> listAllField(Class<?> clazz) {
            List<Field> fieldList = new ArrayList<>();
            while (!clazz.equals(Object.class)) {
                fieldList.addAll(Arrays.asList(clazz.getDeclaredFields()));
                clazz = clazz.getSuperclass();
            }
            return fieldList;
        }
    }

    @RequiredArgsConstructor
    public static class LessManager {
        private final RepositoryRegistry repositoryRegistry;

        private final EntityRegistry entityRegistry;

        private final EntityIdRegistry entityIdRegistry;

        @PostConstruct
        public void init() {
            Map<String, JpaRepository> jpaRepositoryMap = SpringContextHolder.getApplicationContext().getBeansOfType(JpaRepository.class);
            jpaRepositoryMap.values().forEach(jpaRepository -> {
                Type[] types = jpaRepository.getClass().getGenericInterfaces();
                Class<?> entityClass = (Class<?>) ((ParameterizedType) ((Class<?>) types[0]).getGenericInterfaces()[0]).getActualTypeArguments()[0];
                Class<?> entityIdClass = (Class<?>) ((ParameterizedType) ((Class<?>) types[0]).getGenericInterfaces()[0]).getActualTypeArguments()[1];
                repositoryRegistry.registry(entityClass, jpaRepository);
                entityRegistry.registry(entityClass);
                entityIdRegistry.registry(entityClass, entityIdClass);
            });
        }

        public Set<String> listEntityName() {
            return entityRegistry.listEntityName();
        }

        public Optional<JpaRepository<Object, Object>> findRepo(String name) {
            return Optional.ofNullable((JpaRepository<Object, Object>) repositoryRegistry.getRepo(name));
        }

        public Class<?> getEntityClass(String name) {
            return entityRegistry.getEntityClass(name);
        }

        public Object getEntityObj(String name, String t) {
            Class<?> entityClass = entityRegistry.getEntityClass(name);
            return JSON.parseObject(t, entityClass);
        }

        public Object getEntityIdObj(String name, String id) {
            Class<?> entityIdClass = entityIdRegistry.getEntityIdClass(name);
            if (Long.class.isAssignableFrom(entityIdClass)) {
                return Long.valueOf(id);
            } else if (Integer.class.isAssignableFrom(entityIdClass)) {
                return Integer.valueOf(id);
            } else {
                return id;
            }
        }

    }

    public static class RepositoryRegistry {
        private final Map<String, JpaRepository<?, ?>> entityName2Repo = new HashMap<>();

        public <T extends JpaRepository<?, ?>> void registry(Class<?> entityClass, T repo) {
            entityName2Repo.put(underline(entityClass.getSimpleName()), repo);
        }

        public JpaRepository<?, ?> getRepo(String name) {
            return entityName2Repo.get(name);
        }
    }

    public static class EntityRegistry {
        private final Map<String, Class<?>> name2Entity = new HashMap<>();

        public void registry(Class<?> entityClass) {
            name2Entity.put(underline(entityClass.getSimpleName()), entityClass);
        }

        public Class<?> getEntityClass(String name) {
            return name2Entity.get(name);
        }

        public Set<String> listEntityName() {
            return name2Entity.keySet();
        }
    }

    public static class EntityIdRegistry {
        private final Map<String, Class<?>> name2EntityId = new HashMap<>();

        public void registry(Class<?> entityClass, Class<?> entityIdClass) {
            name2EntityId.put(underline(entityClass.getSimpleName()), entityIdClass);
        }

        public Class<?> getEntityIdClass(String name) {
            return name2EntityId.get(name);
        }
    }


    private static String underline(String name) {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < name.length(); ++i) {
            char ch = name.charAt(i);
            if (ch >= 'A' && ch <= 'Z') {
                char chCase = (char) (ch + 32);
                if (i > 0) {
                    buf.append('-');
                }
                buf.append(chCase);
            } else {
                buf.append(ch);
            }
        }
        return buf.toString();
    }
}
